tags:
- Cpp
Part2:Class (Abandoned)
struct
和class
在C语言中,我们常用struct
关键字来定义我们想要的数据类型,但结构体中不能包含函数。这是因为结构体中的变量存储在数据段(如.bss
、.data
)或堆栈上,而函数存储在代码区(.text
)。我们要对结构体的数据进行操作,只能在结构体外定义各种函数,这不仅不利于数据的封装性,而且独立的函数也容易让人一头雾水。(C++中,struct和class的唯一区别就是默认类内/结构体中的属性不同,还有继承)
以下是一个面向过程的“把大象放进冰箱”的操作示例:
#include <stdio.h>
typedef struct {
unsigned int height;
unsigned int weight;
unsigned int age;
} person;
typedef struct {
unsigned int volume;
} elephant;
typedef struct {
unsigned int volume;
} fridge;
void person_cutting_elephant(person p, elephant e) {
/*
cutting operations
*/
printf("Elephant has been cut\n");
}
void person_fill_fridge(person p, elephant e, fridge f) {
/*
filling operations
*/
printf("Fridge has been filled\n");
}
int main() {
person p = {180, 70, 24};
elephant e = {5};
fridge f = {6};
person_cutting_elephant(p, e);
person_fill_fridge(p, e, f);
return 0;
}
在这个例子中,我们定义了人、大象和冰箱三个结构体,并在结构体外编写了相关函数。虽然这样可以完成任务,但数据和操作分离的形式并不直观。当使用结构体,操作函数描述起来的着重点是函数,如:人把大象切开了,其中人是p、大象是e(有了类,我们就可以说p人把e大象切开了)。
而C++提供了class
类描述符,可以将对象的操作(方法函数)放到类内,不仅提升了数据元素的封装性,还使整个过程的思维流程更加通顺易懂。在开发大型项目时,海量的操作函数并不利于项目迭代和操作不同的对象。而类能做什么,只需查看类内成员函数就一目了然了。在写一个面向对象的“把大象放进冰箱”的操作示例之前,我们先学习类的构造函数和析构函数。
类相当于一个新类型,该类型的成员变量可以放在数据段或堆栈上,同样成员函数会存储在代码区(.text
段)。构造函数就是在类对象创建时就在对应内存将数据初始化的成员函数。
&&
)作为参数。移动构造函数会将右值引用对象资源移动到新对象,并”人工地“将源对象的成员变量重置。class person
{
public:
unsigned int height;
unsigned int weight;
unsigned int age;
public:
//默认构造函数
person() :height(0), weight(0), age(0) {
std::cout << "Default constructor has been called." << std::endl;
}
//普通构造函数
person(unsigned int height_, unsigned int weight_, unsigned int age_)
:height(height_), weight(weight_), age(age_) {
std::cout << "A person object is constructed." << std::endl;
}
//复制构造函数
person(const person& other_person)
:height(other_person.height), weight(other_person.weight), age(other_person.age) {
std::cout << "Copy constructor has been called." << std::endl;
}
//移动构造函数
person(person&& other_person)
noexcept:height(other_person.height), weight(other_person.weight), age(other_person.age) {
other_person.height = 0;
other_person.weight = 0;
other_person.age = 0;
std::cout << "Move constructor has been called." << std::endl;
}
~person() {}
};
析构函数(Deconstructor)是C++中的一种特殊成员函数,用于在对象的生命周期结束时执行清理操作。析构函数的名称与类名相同,但前面有一个波浪号(~
),并且没有返回类型和参数,所以不能够重载。
析构函数的作用是释放类对象的资源,只有当类内成员申请内存才需要在析构函数中显式定义delete
操作,否则析构函数不需要释放什么资源。如果类管理其他资源(如文件句柄、网络连接、数据库连接等),需要在析构函数中释放这些资源。
我们忽略大象类、冰箱类的实现和操作的实现细节,以下是一个面向对象的“把大象放进冰箱”的操作示例:
#include<iostream>
class person;
class elephant;
class fridge;
class person
{
public:
unsigned int height;
unsigned int weight;
unsigned int age;
public:
//默认构造函数
person() :height(0), weight(0), age(0) {
std::cout << "Default constructor has been called." << std::endl;
}
//普通构造函数
person(unsigned int height_, unsigned int weight_, unsigned int age_)
:height(height_), weight(weight_), age(age_) {
std::cout << "A person object is constructed." << std::endl;
}
//复制构造函数
person(const person& other_person)
:height(other_person.height), weight(other_person.weight), age(other_person.age) {
std::cout << "Copy constructor has been called." << std::endl;
}
//移动构造函数
person(person&& other_person)
noexcept:height(other_person.height), weight(other_person.weight), age(other_person.age) {
other_person.height = 0;
other_person.weight = 0;
other_person.age = 0;
std::cout << "Move constructor has been called." << std::endl;
}
~person() {}
void person_cut_elephant(const person p, elephant e) ;
void person_fill_fridge(const person p, elephant e, fridge f) ;
};
int main() {
person p(180, 70, 24);
elephant e(5);
fridge f(6);
p.person_cut_elephant();
p.person_fill_fridge();
return 0;
}
这样,我们能明显的感受到前面“人把大象切开了,其中人是p、大象是e(有了类,我们就可以说p人把e大象切开了)。”这句话的含义。
this
,常成员函数和常对象this
this
是 C++ 中的一个关键字,也是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。所谓当前对象,是指正在使用的对象。this
只能用在类的内部,通过 this
可以访问类的所有成员,包括 private、protected、public 属性的。this
的简单用法如下:
#include <iostream>
class person {
int height;
int weight;
int age;
public:
person(int h, int w, int a) :height(h), weight(w), age(a) {
std::cout << "A person object has been added." << std::endl;
}
~person() {}
void getter() {
std::cout << "Height: " << this->height << std::endl << "Weight: "
<< this->weight << std::endl << "Age: " << this->age << std::endl;
}
void setter(int height, int weight, int age) {
this->height = height;// 使用 this->height 表示成员变量
this->weight = weight;
this->age = age;
}
};
int main() {
person Pa(160, 50, 22);
person Pb(175, 60, 20);
Pa.getter();
Pb.getter();
return 0;
}
编译器将this
关键字解释为指向函数所作用的对象的指针。 C++类的本质就是C语言的结构体外加几个类外的函数,C++最后都要转化为C语言来实现,类外的函数就是通过this来指向这个类的。上面的类在 C 语言中的实现如下:
#include <stdio.h>
typedef struct {
int height;
int weight;
int age;
} Person;
void initPerson(Person *this, int height, int weight, int age) {
this->height = height;
this->weight = weight;
this->age = age;
printf("A person object has been added.\n");
}
void getter(const Person *this) {
printf("Height: %d\nWeight: %d\nAge: %d\n",
this->height, this->weight, this->age);
}
void setter(Person *this, int height, int weight, int age) {
this->height = height; // 使用 p->height 表示成员变量
this->weight = weight;
this->age = age;
}
int main() {
Person Pa, Pb;
initPerson(&Pa, 160, 50, 22);
initPerson(&Pb, 175, 60, 20);
getter(&Pa);
getter(&Pb);
return 0;
}
this
有很多功能是单纯的指针无法满足的。比如每个类函数的参数根本没有名叫this
的指针。这是编译器赋予的功能。
const
除了修饰变量,还可以用来修饰类内函数。用 const
修饰的函数我们称为常成员函数。C++规定常成员函数在调用时不会修改类的成员变量。
class MyClass {
public:
int getValue() const { // 常成员函数
return value;
}
void setValue(int val) {
value = val;
}
private:
int value;
};
函数括号后的const
实际上是为了修饰this
,常成员函数会隐式地转换为:
void outPut(const myClass* const myThis){
std::cout << myThis->name << std::endl;
}
但由于this
是隐式的,所以将const
放到括号外。
同样,加入const
关键字就可以定义一个常对象。理解起来也很方便,常对象的对象变量是不可改变的。这样一来,常对象和普通对象可以使用的类内函数就有一些区别。即 常对象只能使用常成员函数。在函数重载的情况下,普通对象只能使用普通成员函数,常对象只能使用常成员函数。我们举例说明一下:
#include <iostream>
class person {
int height;
int weight;
int age;
public:
person(int h, int w, int a) :height(h), weight(w), age(a) {
std::cout << "A person object has been added." << std::endl;
}
~person() {}
void getter() {
std::cout << "Height: " << height << std::endl << "Weight: "
<< weight << std::endl << "Age: " << age << std::endl << this << std::endl;
}
void getter()const {
std::cout << "Height: " << height << std::endl << "Weight: "
<< weight << std::endl << "Age: " << age << std::endl << this << std::endl;
std::cout << "this is a const func" << std::endl;
}
};
int main() {
person Pa(160, 50, 22);
const person Pb(175, 60, 20);
Pa.getter();
Pb.getter();
return 0;
}
inline
, mutable
, default
, delete
关键字我们用 inline
关键字来指定某一个函数为内联函数。我们知道,当函数被调用时,会在栈空间里生成栈帧。函数的调用和返回对应着栈帧的生成与销毁,这往往意味着开销。内联函数就是让调用函数将被调用函数看作自身的一部分(即被调用函数不需要生成栈帧,空间换时间),节省了调用其他函数时生成和销毁栈帧的时间开销。因此,内联函数往往设置为较为短小的函数,因为当函数很大时,内联会导致代码膨胀,增加指令缓存的压力,反而可能降低性能。
我们在使用inline
关键字时需要有以下几点注意:
#include<iostream>
class test {
public:
inline void print();
void print2();
void print3(){
std::cout << "print3" << std::endl;
}
};
void test::print() {
std::cout << "hello" << std::endl;
}
inline void test::print2(){
std::cout << "print2" << std::endl;
}
int main() {
test test1;
test1.print(); //test1.print(); 不是内联函数
test1.print2();//test1.print2();是内联函数
test1.print3();//是内联函数
}
mutable
关键字mutable
可变的,这个关键字与const
相对。const
关键字突出的是“常”而mutable
关键字突出的是“变”。我们延续上面的例子来看看mutable
可以做什么:
#include<iostream>
class test {
mutable unsigned int print_Count;
public:
test(unsigned int Count_init) :print_Count(Count_init) {}
inline void print()const;
};
void test::print()const {
std::cout << "hello" << std::endl;
print_Count++;
std::cout << print_Count << std::endl;
}
int main() {
test test1(20);
test1.print();
test1.print();
}
我们看到,即使我们定义了一个常成员函数,我们依然可以因为变量是mutable
修饰的而修改print_Count
的值。
mutable
关键字一般万不得已才会使用,为了提高程序设计应当避免使用mutable
关键字。另外注意,mutable不能修饰静态成员变量和常成员变量。
default
关键字提高代码的可读性。
delete
关键字delete
关键字最基本的用途就是释放掉申请的堆内存,如:
//删去单个对象
int* ptr = new int;
delete ptr;
//删去对象数组
int* ptr = new int[10];
delete[] ptr;
但在C++ 11,delete
关键字可以用于disable特定函数,用于阻止某些特定操作。比如,我们就可以在类内disable复制构造函数,delete
关键字也可以用于disable类外的函数。
C++提供friend
关键字来声明友元函数和友元类。如:
#include <iostream>
class person;
class dog;
void gym(person& p);
class person {
int height;
int weight;
int age;
public:
friend class dog;
friend void gym(person& p);
person(int h, int w, int a) :height(h), weight(w), age(a) {
std::cout << "A person object has been added." << std::endl;
}
~person() {}
void getter()const {
std::cout << "Height: " << height << std::endl << "Weight: "
<< weight << std::endl << "Age: " << age << std::endl << this << std::endl;
}
};
class dog {
public:
void bark_person(person& p) {
p.age = 0;
p.height = 0;
p.weight = 0;
p.getter();
}
};
void gym(person& p) {
p.weight -= 5;
p.getter();
}
int main() {
person Pa(160, 50, 22);
person Pb(175, 65, 30);
dog D;
D.bark_person(Pb);
gym(Pa);
return 0;
}
我们看到,友元函数和友元类可以任意操作person
类中的任意成员,无论成员是什么属性。这样会破坏类的封装性,使得类内私有成员在友元函数和友元类下一览无余,而友元函数和友元类可能只是需要访问类内的某几个私有成员。我们可以用getter
来代替友元对象,但也各有利弊。有些运算符的重载必须用到友元的功能,我们下节课会介绍。
在C++程序设计课程中,我们就听到多C++面向对象的三大特征——封装、继承和多态。这节课,我们来简单了解一下继承。
我们先试想一个场景,假如我们要开发一款游戏,主人公需要使用法杖来过五关斩六将。火之杖、水之杖、冰之杖、土之杖......这么多法杖我们开发时需要如下这样做么?
class fire_wand{...};
class water_wand{...};
class ice_wand{...};
class earth_wand{...};
class air_wand{...};
...
我们显然不会这也做,将法杖一个个枚举起来既延长了开发时长,也不利于后面的拓展。C++提供了子类对父类的继承派生,这就大大减少了我们的工作量。假如父类法杖有3个属性,加入继承后,子类只需要实现子类法杖的属性,有关父类的各种属性只需要继承就好了。代码如下:
class wand {
char name[10];
char element[10];
unsigned int length;
};
class fire_wand :public wand {//子类fire_wand继承父类wand的所有成员变量
unsigned char* elem_paricle;
int particle_width;
int particle_height;
};
...
接着我们来演示一下父子类的构造和析构函数。在此之前,我们需要注意:
#include <iostream>
#include <cstring>
class wand {
std::string name;
std::string element;
unsigned length;
public:
wand(std::string name_, std::string element_, unsigned length_)
:name(name_), element(element_), length(length_) {
std::cout << "Father class constructor." << std::endl;
}
~wand() {
std::cout << "Father class deconstrutor." << std::endl;
}
};
class fire_wand :public wand {
unsigned char* elem_paricle;
int particle_width;
int particle_height;
public:
fire_wand(std::string name_, std::string element_, unsigned length_,
unsigned char* elem_particle_, int parti_width_, int parti_height_)
:wand(name_, element_, length_), elem_paricle(elem_particle_),
particle_width(parti_width_), particle_height(parti_height_) {
std::cout << "Child class constructor." << std::endl;
}
~fire_wand() {
std::cout << "Child class deconstructor" << std::endl;
}
};
int main() {
unsigned char parti_data[2 * 35] = { 0 };
fire_wand fw("Blacken blast", "Fire", 20, parti_data, 2, 35);
wand* w = &fw;//父类指针指向子类对象
return 0;
}
polymorphism — providing a single interface to entities of different types. virtual functions provide dynamic (run-time) polymorphism through an interface provided by a base class. Overloaded functions and templates provide static (compile-time) polymorphism.
多态(Polymorphism)是面向对象编程(OOP)的一个核心概念。它允许同一个接口调用不同类型的对象,并根据对象的实际类型执行相应的操作。多态性可以分为两种类型:编译时多态性(静态多态性)和运行时多态性(动态多态性)。
编译时多态性通过函数重载(Overload) 和模板实现。在编译期间,编译器根据函数的参数类型和数量来确定调用哪个函数。这种多态性在编译时就已经确定了函数调用的地址,因此称为早绑定(early binding)。
重载函数虽然函数名是相同的,但由于不同的参数列表(不同的参数类型和数量)使得编译器能够区分这些函数。编译器在编译时根据传递的参数类型和数量来确定调用哪个重载函数。
运行时多态性通过虚函数实现。虚函数允许子类重写(Override) 基类中的函数。当通过基类指针或引用调用虚函数时,实际调用的是子类的实现。这种多态性在运行时才确定函数调用的地址,因此称为晚绑定(late binding)。
override
虚函数实现了运行时(Run-time) 的多态性。函数的多态性就意味着父类对象可以调用子类对象中的成员函数。我们之前说过,因为子类对象包含父类的所有成员变量,所以父类对象的指针是可以指向子类对象的。这样父类的析构函数就必须是虚函数,不然就可能会造成内存泄漏。
每个有虚函数的类都会有一个虚函数表,对象其实就是指向虚函数表的指针,编译时编译器只告诉了程序会在运行时查找虚函数表的对应函数。每个类都会有自己的虚函数表,所以当父类指针引用的是子类虚函数表时,自然调用的就是子类的函数。
override
重写关键字override
关键字用于在子类中重写基类中的虚函数。它告诉编译器该函数是用来重写基类中的虚函数的,如果没有正确匹配基类中的虚函数,编译器会报错。这有助于避免由于拼写错误或参数不匹配而导致的意外行为。
我们举例说明:
class wand {
public:
wand(){}
virtual ~wand() {}
virtual void test() {}
};
class fire_wand : public wand {
public:
fire_wand(){}
virtual ~fire_wand()override {}
virtual void test()override{}//标准写法
virtual void teste(){}//,函数名写错了,但不会报错
virtual void teste()override{}//报错,因为父类中没有能够匹配的虚函数
};
int main() {
return 0;
}
由于父类中并没有申请任何堆内存,我们就将父类析构函数的提示输出给注释掉。我们让父类的test
函数输出一串字符串,让子类test
函数申请一段堆内存。由于父类test
是虚函数,所以子类中的test
函数即使不加virtual
关键字,编译器也会隐式地认为这个函数是虚函数。一旦父类对象调用了子类的test
函数,就会动态申请一段堆内存。所以父类的析构函数必须也是虚函数,不然会造成内存泄漏。
#include <iostream>
#include <cstring>
class wand {
std::string name;
std::string element;
unsigned length;
public:
wand(std::string name_, std::string element_, unsigned length_)
:name(name_), element(element_), length(length_) {
std::cout << "Father class constructor." << std::endl;
}
virtual ~wand() { // 虚析构函数
//std::cout << "Father class destructor." << std::endl;
}
virtual void test() {
std::cout << "Base class" << std::endl;
}
};
class fire_wand : public wand {
unsigned char* elem_paricle;
int particle_width;
int particle_height;
fire_wand* fw_ptr = NULL;
public:
fire_wand(std::string name_, std::string element_, unsigned length_,
unsigned char* elem_particle_, int parti_width_, int parti_height_)
: wand(name_, element_, length_), elem_paricle(elem_particle_),
particle_width(parti_width_), particle_height(parti_height_) {
std::cout << "Child class constructor." << std::endl;
}
virtual ~fire_wand()override {
if (fw_ptr == NULL) {
std::cout << "Child class destructor" << std::endl;
}
else {
delete fw_ptr;
std::cout << "Heap space freed." << std::endl;
}
}
void test(){
unsigned char parti_data[2 * 35] = { 0 };
fw_ptr = new fire_wand("Blacken blast", "Fire", 20, parti_data, 2, 35);
std::cout << "Derived class" << std::endl;
}
};
int main() {
unsigned char parti_data[2 * 35] = { 0 };
wand* wand_ptr = new fire_wand("Blacken blast", "Fire", 20, parti_data, 2, 35);
wand_ptr->test();
delete wand_ptr;
return 0;
}
代码执行后结果如下:
Father class constructor.
Child class constructor.
Father class constructor.
Child class constructor.
Derived class
Child class destructor
Heap space freed.
该函数为虚函数:父类指针调用的是子类的成员函数。
该函数不是虚函数:父类指针调用的是父类的成员函数。
之前说过,如果静态变量未初始化就会载入bss.
段并在程序加载时初始化为0。但是静态成员变量必须在类外进行初始化。这是因为静态成员变量属于整个类。无论创建多少个对象,静态成员变量在内存中也只有一份拷贝,存储在静态存储区中,而不在堆栈区。在类外初始化可以避免静态成员变量多次初始化,保证了静态成员变量的唯一性。(不能用构造函数初始化)
而且由于静态成员变量为整个类所共享的特性,所以它可以通过类名来调用,当然也能用类对象调用。创建一个静态成员变量如下所示:
#include <iostream>
class Test {
public:
static int var;
static const int vari = 10;//可以
};
int Test::var = 0;
int main() {
Test t;
std::cout << Test::var << std::endl;
std::cout << t.var << std::endl;
return 0;
}
静态成员函数和静态成员变量一样,可以用类名调用Test::static_func();
,同样可以用类对象调用。静态成员函数只能访问静态成员变量,由于没有this
指针,所以也不能访问非静态成员变量。也由于静态的特性,使得静态成员函数不需要生成对象就可调用。
虽然静态成员函数和普通成员函数都可以访问静态成员变量,但是由于静态成员函数可以在不创建类对象的情况下被调用,所以多用静态成员函数调用静态成员变量。
代码示例:
#include <iostream>
class Test {
static int var;
public:
static void getter() {
std::cout << var << std::endl;
}
};
int Test::var = 0;
int main() {
Test::getter();//没有创建实例对象。
return 0;
}
父类子类继承中,父类唯一的作用就是被子类继承。父类不产生任何对象,父类的虚函数实现过程也是没有意义的,唯一的作用就是为子类重写。为了不让这些无意义的代码占用内存空间,纯虚函数的语法就诞生了。
纯虚函数是一种特殊的虚函数,它没有实现,只提供接口。纯虚函数的语法是将函数声明为 = 0
。包含纯虚函数的类称为抽象类,不能实例化对象。抽象类的主要作用是作为其他类的基类,提供一个接口框架。
在法杖的例子中,我们用virtual void test() = 0;
将test()
函数定义成纯虚函数,这样,这条语句就不会产生任何的函数对象,编译器会忽略之。由于子类还是需要父类构造函数和析构函数,所以构造函数和析构函数不能声明成纯虚函数。
class wand {
std::string name;
std::string element;
unsigned length;
public:
wand(std::string name_, std::string element_, unsigned length_)
:name(name_), element(element_), length(length_) {
std::cout << "Father class constructor." << std::endl;
}
virtual ~wand() { // 虚析构函数
std::cout << "Father class destructor." << std::endl;
}
virtual void test() = 0;
};
virtual void test() = 0;
声明了一个纯虚函数,这意味着 wand
类是一个抽象类,不能实例化对象。wand
类包含纯虚函数,它被认为是抽象类,不能直接创建 wand
类的对象。
test()
,所以下面的语句都会是错误的。 wand* wand_ptr = new fire_wand("Blacken blast", "Fire", 20,);
wand* wand_ptr("Blacken blast", "Fire", 20);
fire_wand
类实现了 wand
类中的纯虚函数 test()
,因此 fire_wand
类可以实例化对象。